Drush command porting guide to the new dr Drupal core CLI

Last updated on
7 July 2026

This documentation needs review. See "Help improve this page" in the sidebar.

Drush and the Drupal core CLI script have a modern successor!

Drupal core's command-line capabilities are expanding with the introduction of a native extensible Drupal Core CLI. Historically, core/scripts/drupal provided a limited command-line interface for core-specific tasks, while the community-maintained Drush project became the de facto standard for Drupal site management. To standardize and improve developer experience, Drupal has introduced a new, built-in CLI accessible via vendor/bin/dr. This new implementation refactors existing core commands and provides a standardized way for module developers to create discoverable commands using modern Symfony attributes, effectively becoming a replacement for both the previously core-provided script and the robust functionality previously exclusive to Drush.

There were only a few Drupal core commands before and most developers would be familiar with Drush commands. Many Drupal extensions provide them. This guide aims to provide information on how to port Drush commands to the new core solution and how to keep commands compatible with both the core solution and Drush in the transition period.

Both Drupal Core CLI (vendor/bin/dr) and Drush are built on top of the Symfony Console component, but they handle command registration, annotations/attributes, and boilerplate slightly differently.

Here is a comparison, what you need to know as a Drush command developer, and how to maintain compatibility between both ecosystems.

Key differences between Drupal CLI and Drush

Feature

Drupal Core CLI (vendor/bin/dr)

Drush (vendor/bin/drush)

Directory

src/Command/

src/Drush/Commands/ or src/Commands/

Base Class

Extends the Symfony Command component class OR is a plain class with __invoke()

Typically extends Drush\Commands\DrushCommands

Registration

Auto-discovered by Drupal's Compiler Pass. No .services.yml needed if using attributes.

Registered explicitly in drush.services.yml.

Attributes

Uses Symfony attributes (#[AsCommand], #[Argument], #[Option], #[Ask])

Uses Drush/Consolidation attributes (#[CLI\Command], #[CLI\Argument]) or legacy annotations.

Execution Method

execute(InputInterface $input, OutputInterface $output) or __invoke(...)

Any custom method name mapped to the command via the attribute.

What Drush Command Writers Should Know

If you are used to writing Drush commands, moving to the new Drupal Core CLI introduces a few changes:

  • No drush.services.yml required: In Drush, you usually have to wire up your command class in a specific drush.services.yml file. In the new Drupal Core CLI, any class inside your module's src/Command/ directory that uses the #[AsCommand] attribute is automatically scanned, autowired, and registered via ConsoleCompilerPass.

  • Invokable Classes: Drush command files often contain multiple commands (multiple methods inside one DrushCommands class). Drupal's new standard highly encourages Single Responsibility using the __invoke() method on a plain class, creating one class per command.

  • Arguments and Options Injection: In Drush, arguments and options are passed directly as parameters to your method (e.g., public function myCommand($name, $options = ['shout' => false])). In Drupal Core CLI's __invoke pattern, you can use PHP attributes directly on the parameters (e.g., #[Argument('The name')] string $name).

  • Interactive Prompts Built-in: Core CLI utilizes the new Symfony #[Ask] attribute for interactive prompts natively on the parameter definition. In Drush, you typically have to drop down into $this->io()->ask() inside the method body.

  • Return Types: Standard Symfony commands must return an integer (Command::SUCCESS or Command::FAILURE). Drush commands are heavily integrated with Consolidation/OutputFormatters and frequently return arrays, objects, or strings that Drush automatically formats into tables or JSON based on the user's --format flag.

Use the Service Wrapper Pattern to Make Commands Compatible with Both

Because Drush and Drupal Core CLI expect different metadata (Symfony attributes vs. Consolidation attributes) and slightly different execution flows (returning an int vs returning formatted data), a single class acting as both a Drush command and a Core CLI command is generally discouraged and very messy to maintain.

If you want to provide your command to both ecosystems, the best practice is the Service Wrapper Pattern. Extract the actual business logic of your command into a standard Drupal Service. Then, write two very thin "wrapper" command classes—one for Drush and one for Core CLI.

Step 1: Create the Business Logic Service

// src/MyModuleLogic.php
namespace Drupal\my_module;

class MyModuleLogic {
  public function doSomething(string $name): string {
   return "Hello, $name!";
  }
}

(Register this normally in my_module.services.yml)

Step 2: Create the Drupal Core CLI Command (vendor/bin/dr)

// src/Command/HelloCommand.php
namespace Drupal\my_module\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use Drupal\my_module\MyModuleLogic;

#[AsCommand(name: 'my_module:hello', description: 'Says hello.')]
class HelloCommand {
  public function __construct(private readonly MyModuleLogic $logic) {}

  public function __invoke(OutputInterface $output, #[Argument] string $name): int {
    $result = $this->logic->doSomething($name);
    $output->writeln($result);
    return Command::SUCCESS;
  }
}

Step 3: Create the Drush Command (vendor/bin/drush)

// src/Drush/Commands/HelloDrushCommands.php<
namespace Drupal\my_module\Drush\Commands;

use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Drupal\my_module\MyModuleLogic;

class HelloDrushCommands extends DrushCommands {
  public function __construct(private readonly MyModuleLogic $logic) {
    parent::__construct();
  }

  #[CLI\Command(name: 'my_module:hello', aliases: ['mm-hello'])]
  #[CLI\Argument(name: 'name', description: 'The name to greet')]
  public function sayHello(string $name) {
    $result = $this->logic->doSomething($name);
    $this->logger()->success($result);
  }
}

(Register this in drush.services.yml)

Why this is the best approach

  1. Separation of Concerns: Your business logic remains independent of the CLI framework calling it.

  2. Formatting features: Drush has advanced table formatting and pipeline outputs that Symfony standard CLI doesn't natively share out-of-the-box in the same way. By splitting them, you can utilize Drush's rich formatting in the Drush wrapper, and standard Symfony formatting in the Core wrapper.

  3. Future Proof: If either Drush or Drupal Core updates their command implementation standards, you only have to update the thin wrapper files, not your core logic.

Naming Conventions and Best Practices

  • Naming Conventions: Modules must use their module name as the namespace, substituting underscores with hyphens (e.g., my_module becomes my-module:my-command). The core and core module namespaces are reserved for Drupal core to prevent future conflicts.

  • Logging: The dr CLI logs output to the logger.console (specifically Drupal\Core\Command\DrupalConsoleLogger).

  • Status and Support: The new CLI is currently considered @internal and experimental. For support or to track progress, join the #cli-in-core channel on Drupal Slack or follow discussions in the [meta] CLI in Core community initiative issue.

Help improve this page

Page status: Needs review

You can: